Skip to content

feat: add chat.getUser() for cross-platform user lookups#391

Merged
dancer merged 7 commits intomainfrom
feat/get-user
Apr 29, 2026
Merged

feat: add chat.getUser() for cross-platform user lookups#391
dancer merged 7 commits intomainfrom
feat/get-user

Conversation

@bensabic
Copy link
Copy Markdown
Contributor

  • Add UserInfo type and optional getUser() method to the Adapter interface
  • Implement on Slack, Discord, Google Chat, GitHub, Linear, and Telegram adapters
  • Returns null when user not found, throws ChatError("NOT_SUPPORTED") for adapters without support
  • Add "Who Am I" button to the example app
  • Add docs with API reference, UserInfo type table, and error handling examples

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chat Ready Ready Preview, Comment, Open in v0 Apr 29, 2026 1:16am
chat-sdk-nextjs-chat Ready Ready Preview, Comment, Open in v0 Apr 29, 2026 1:16am

@bensabic
Copy link
Copy Markdown
Contributor Author

@heyitsaamir any chance we can add Teams support for this?

@bensabic bensabic requested a review from dancer April 16, 2026 03:34
Comment thread packages/chat/src/chat.ts
@heyitsaamir
Copy link
Copy Markdown
Contributor

@heyitsaamir any chance we can add Teams support for this?

It should be possible!

  const member = await app.api.conversations
    .members(activity.conversation.id)
    .getById(activity.from.id);

Lmk if that doesn't work!

@heyitsaamir
Copy link
Copy Markdown
Contributor

So the above will get you the member, but it requires the conversation id. Members in Teams aren't global members, but more based on membership of a conversation.

If you want global members, you can do that via graph, but it'll require some graph permissions i believe (User.Read.All).
Then call it like:

const graphUser = await app.graph.call(endpoints.users.get({ 'user-id': activity.from.aadObjectId! }));

@bensabic
Copy link
Copy Markdown
Contributor Author

So the above will get you the member, but it requires the conversation id. Members in Teams aren't global members, but more based on membership of a conversation.

If you want global members, you can do that via graph, but it'll require some graph permissions i believe (User.Read.All). Then call it like:

const graphUser = await app.graph.call(endpoints.users.get({ 'user-id': activity.from.aadObjectId! }));

Ah thank you for this! Do you by any chance have time to push it to the PR? Mainly ask as you'd know exactly which methods to use and what to troubleshoot accordingly

@heyitsaamir
Copy link
Copy Markdown
Contributor

heyitsaamir commented Apr 16, 2026

So the above will get you the member, but it requires the conversation id. Members in Teams aren't global members, but more based on membership of a conversation.

If you want global members, you can do that via graph, but it'll require some graph permissions i believe (User.Read.All). Then call it like:

const graphUser = await app.graph.call(endpoints.users.get({ 'user-id': activity.from.aadObjectId! }));

Ah thank you for this! Do you by any chance have time to push it to the PR? Mainly ask as you'd know exactly which methods to use and what to troubleshoot accordingly

Give me a couple of days :). I can include it after this PR goes in?

@bensabic
Copy link
Copy Markdown
Contributor Author

So the above will get you the member, but it requires the conversation id. Members in Teams aren't global members, but more based on membership of a conversation.

If you want global members, you can do that via graph, but it'll require some graph permissions i believe (User.Read.All). Then call it like:

const graphUser = await app.graph.call(endpoints.users.get({ 'user-id': activity.from.aadObjectId! }));

Ah thank you for this! Do you by any chance have time to push it to the PR? Mainly ask as you'd know exactly which methods to use and what to troubleshoot accordingly

Give me a couple of days :). I can include it after this PR goes in?

That'd be perfect, thank you so much!

bensabic and others added 6 commits April 29, 2026 01:39
Add UserInfo type and optional getUser() method to the Adapter interface.
Implement on Slack (extends existing lookupUser with email/avatar),
Discord, Google Chat, GitHub, Linear, and Telegram adapters.

Add "Who Am I" button to the example app demonstrating the feature.
Update docs with getUser API reference and usage examples.
- slack: return null from lookupUser on failure instead of fallback
  object, removing the isBot === undefined sentinel in getUser
- slack: use image_192 instead of image_72 for better avatar quality
- gchat: cache avatarUrl from webhook sender payload
- gchat: return avatarUrl in getUser response
- gchat: fix tests to use current cache format with isBot field
- docs: document null return, fix example to use message.author
* feat(adapter-teams): add getUser() via Microsoft Graph API

- Cache aadObjectId from activity.from during webhook handling
- Implement getUser() using Graph GET /users/{user-id} endpoint
- Requires User.Read.All application permission
- Returns null gracefully when user hasn't interacted or Graph call fails

* docs: add getUser() section to Teams adapter README
@dancer dancer closed this Apr 29, 2026
@dancer dancer reopened this Apr 29, 2026
@dancer dancer requested a review from a team as a code owner April 29, 2026 02:22
@bensabic bensabic closed this Apr 29, 2026
@bensabic bensabic reopened this Apr 29, 2026
@dancer dancer merged commit a520797 into main Apr 29, 2026
20 checks passed
@dancer dancer deleted the feat/get-user branch April 29, 2026 14:58
patrick-chinchill pushed a commit to Chinchill-AI/chat-sdk-python that referenced this pull request May 9, 2026
- Slack `_lookup_user`: detect empty `{user: {}}` success payload and
  return the `_lookup_failed` sentinel instead of caching a
  `UserInfo(Uxxx, Uxxx, Uxxx)` shape that `get_user` would convert into
  a non-null fallback. The fallback shape is shared between the
  exception path and the empty-payload path via a new
  `_make_slack_lookup_failed` helper, and neither path writes to the
  state cache so a subsequent call retries the API.
- Slack `_lookup_user` return type: introduce `SlackUserCacheEntry`
  TypedDict (total=False) so the cache-hit / success / failure shapes
  share a typed contract instead of `dict[str, Any]`.
- Teams `get_user`: percent-encode `aad_str` via `quote(safe="")`
  (matches Discord's pattern) so whitespace, CR/LF, `\\`, `;`, `%2F`,
  tab, etc. cannot escape the `/v1.0/users/` path segment. The
  structural-splitter reject list (`/`, `?`, `#`) stays as a fast-path
  reject before the encoding pass.
- Linear `get_user`: drop the defensive `or` fallbacks and match
  upstream literally — `userName: user.displayName, fullName:
  user.name` (vercel/chat#391).

Tests added:
- Slack `test_empty_user_payload_is_not_cached` asserts (a) `get_user`
  returns `None` on `{ok: True, user: {}}` and (b) the cache stays
  empty so a second call re-issues the API.
- Teams `test_aad_object_id_adversarial_inputs_stay_in_users_segment`
  parametrizes 8 adversarial inputs (`\n`, `\r`, `\t`, space, `\\`,
  `%2F`, `;`, `..`) and asserts each is either rejected or
  percent-encoded such that the resulting URL stays under
  `https://graph.microsoft.com/v1.0/users/`.

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants